1784be1533b95645b843523a2230f8fb4674f543
[nextcloud-desktop.git] /
1 //
2 //  ShareTableViewDataSource.swift
3 //  FileProviderUIExt
4 //
5 //  Created by Claudio Cambra on 27/2/24.
6 //
7
8 import AppKit
9 import FileProvider
10 import NextcloudKit
11 import NextcloudFileProviderKit
12 import NextcloudCapabilitiesKit
13 import OSLog
14
15 class ShareTableViewDataSource: NSObject, NSTableViewDataSource, NSTableViewDelegate {
16     private let shareItemViewIdentifier = NSUserInterfaceItemIdentifier("ShareTableItemView")
17     private let shareItemViewNib = NSNib(nibNamed: "ShareTableItemView", bundle: nil)
18     private let reattemptInterval: TimeInterval = 3.0
19
20     let kit = NextcloudKit.shared
21
22     var uiDelegate: ShareViewDataSourceUIDelegate?
23     var sharesTableView: NSTableView? {
24         didSet {
25             sharesTableView?.register(shareItemViewNib, forIdentifier: shareItemViewIdentifier)
26             sharesTableView?.rowHeight = 42.0  // Height of view in ShareTableItemView XIB
27             sharesTableView?.dataSource = self
28             sharesTableView?.delegate = self
29             sharesTableView?.reloadData()
30         }
31     }
32     var capabilities: Capabilities?
33
34     private(set) var itemURL: URL?
35     private(set) var itemServerRelativePath: String?
36     private(set) var shares: [NKShare] = [] {
37         didSet { Task { @MainActor in sharesTableView?.reloadData() } }
38     }
39     private(set) var userAgent: String = "Nextcloud-macOS/FileProviderUIExt"
40     private(set) var account: Account? {
41         didSet {
42             guard let account = account else { return }
43             kit.appendSession(
44                 account: account.ncKitAccount,
45                 urlBase: account.serverUrl,
46                 user: account.username,
47                 userId: account.username,
48                 password: account.password,
49                 userAgent: userAgent,
50                 nextcloudVersion: 25,
51                 groupIdentifier: ""
52             )
53         }
54     }
55
56     func loadItem(url: URL) {
57         itemServerRelativePath = nil
58         itemURL = url
59         Task {
60             await reload()
61         }
62     }
63
64     func reattempt() {
65         DispatchQueue.main.async {
66             Timer.scheduledTimer(withTimeInterval: self.reattemptInterval, repeats: false) { _ in
67                 Task { await self.reload() }
68             }
69         }
70     }
71
72     func reload() async {
73         guard let itemURL else {
74             presentError("No item URL, cannot reload data!")
75             return
76         }
77         guard let itemIdentifier = await withCheckedContinuation({
78             (continuation: CheckedContinuation<NSFileProviderItemIdentifier?, Never>) -> Void in
79             NSFileProviderManager.getIdentifierForUserVisibleFile(
80                 at: itemURL
81             ) { identifier, domainIdentifier, error in
82                 defer { continuation.resume(returning: identifier) }
83                 guard error == nil else {
84                     self.presentError("No item with identifier: \(error.debugDescription)")
85                     return
86                 }
87             }
88         }) else {
89             presentError("Could not get identifier for item, no shares can be acquired.")
90             return
91         }
92
93         do {
94             let connection = try await serviceConnection(url: itemURL, interruptionHandler: {
95                 Logger.sharesDataSource.error("Service connection interrupted")
96             })
97             guard let serverPath = await connection.itemServerPath(identifier: itemIdentifier),
98                   let credentials = await connection.credentials() as? Dictionary<String, String>,
99                   let convertedAccount = Account(dictionary: credentials),
100                   !convertedAccount.password.isEmpty
101             else {
102                 presentError("Failed to get details from File Provider Extension. Retrying.")
103                 reattempt()
104                 return
105             }
106             let serverPathString = serverPath as String
107             itemServerRelativePath = serverPathString
108             account = convertedAccount
109             await sharesTableView?.deselectAll(self)
110             capabilities = await fetchCapabilities()
111             guard capabilities != nil else { return }
112             guard capabilities?.filesSharing?.apiEnabled == true else {
113                 presentError("Server does not support shares.")
114                 return
115             }
116             guard let account else {
117                 presentError("Account data is unavailable, cannot reload data!")
118                 return
119             }
120             guard let itemMetadata = await fetchItemMetadata(
121                 itemRelativePath: serverPathString, account: account, kit: kit
122             ) else {
123                 presentError("Unable to retrieve file metadata...")
124                 return
125             }
126             guard itemMetadata.permissions.contains("R") == true else {
127                 presentError("This file cannot be shared.")
128                 return
129             }
130             shares = await fetch(
131                 itemIdentifier: itemIdentifier, itemRelativePath: serverPathString
132             )
133             shares.append(Self.generateInternalShare(for: itemMetadata))
134         } catch let error {
135             presentError("Could not reload data: \(error), will try again.")
136             reattempt()
137         }
138     }
139
140     private func fetch(
141         itemIdentifier: NSFileProviderItemIdentifier, itemRelativePath: String
142     ) async -> [NKShare] {
143         Task { @MainActor in uiDelegate?.fetchStarted() }
144         defer { Task { @MainActor in uiDelegate?.fetchFinished() } }
145
146         let rawIdentifier = itemIdentifier.rawValue
147         Logger.sharesDataSource.info("Fetching shares for item \(rawIdentifier, privacy: .public)")
148
149         guard let account else {
150             self.presentError("NextcloudKit instance or account is unavailable, cannot fetch shares!")
151             return []
152         }
153
154         let parameter = NKShareParameter(path: itemRelativePath)
155
156         return await withCheckedContinuation { continuation in
157             kit.readShares(
158                 parameters: parameter, account: account.ncKitAccount
159             ) { account, shares, data, error in
160                 let shareCount = shares?.count ?? 0
161                 Logger.sharesDataSource.info("Received \(shareCount, privacy: .public) shares")
162                 defer { continuation.resume(returning: shares ?? []) }
163                 guard error == .success else {
164                     self.presentError("Error fetching shares: \(error.errorDescription)")
165                     return
166                 }
167             }
168         }
169     }
170
171     private static func generateInternalShare(for file: NKFile) -> NKShare {
172         let internalShare = NKShare()
173         internalShare.shareType = NKShare.ShareType.internalLink.rawValue
174         internalShare.url = file.urlBase +  "/index.php/f/" + file.fileId
175         internalShare.account = file.account
176         internalShare.displaynameOwner = file.ownerDisplayName
177         internalShare.displaynameFileOwner = file.ownerDisplayName
178         internalShare.path = file.path
179         return internalShare
180     }
181
182     private func fetchCapabilities() async -> Capabilities? {
183         guard let account else {
184             self.presentError("Could not fetch capabilities as account is invalid.")
185             return nil
186         }
187
188         return await withCheckedContinuation { continuation in
189             kit.getCapabilities(account: account.ncKitAccount) { account, data, error in
190                 guard error == .success, let capabilitiesJson = data?.data else {
191                     self.presentError("Error getting server caps: \(error.errorDescription)")
192                     continuation.resume(returning: nil)
193                     return
194                 }
195                 Logger.sharesDataSource.info("Successfully retrieved server share capabilities")
196                 continuation.resume(returning: Capabilities(data: capabilitiesJson))
197             }
198         }
199     }
200
201     private func presentError(_ errorString: String) {
202         Logger.sharesDataSource.error("\(errorString, privacy: .public)")
203         Task { @MainActor in self.uiDelegate?.showError(errorString) }
204     }
205
206     // MARK: - NSTableViewDataSource protocol methods
207
208     @objc func numberOfRows(in tableView: NSTableView) -> Int {
209         shares.count
210     }
211
212     // MARK: - NSTableViewDelegate protocol methods
213
214     @objc func tableView(
215         _ tableView: NSTableView, viewFor tableColumn: NSTableColumn?, row: Int
216     ) -> NSView? {
217         let share = shares[row]
218         guard let view = tableView.makeView(
219             withIdentifier: shareItemViewIdentifier, owner: self
220         ) as? ShareTableItemView else {
221             Logger.sharesDataSource.error("Acquired item view from table is not a share item view!")
222             return nil
223         }
224         view.share = share
225         return view
226     }
227
228     @objc func tableViewSelectionDidChange(_ notification: Notification) {
229         guard let selectedRow = sharesTableView?.selectedRow, selectedRow >= 0 else {
230             Task { @MainActor in uiDelegate?.hideOptions(self) }
231             return
232         }
233         let share = shares[selectedRow]
234         Task { @MainActor in uiDelegate?.showOptions(share: share) }
235     }
236 }